use std::collections::HashMap;

#[derive(PartialEq, Debug)]
enum Instruction {
    Left,
    Right,
}

#[derive(PartialEq, Debug)]
struct Choise {
    left_node: String,
    right_node: String,
}

impl Choise {
    #[allow(unused)]
    pub fn new(left_node: String, right_node: String) -> Self {
        Self { left_node, right_node }
    }
}

impl From<&str> for Choise {
    fn from(value: &str) -> Self {
        let value = value.trim();
        // First is '(', last is ')'
        let mut tokens = value.split(",");

        let left_str = tokens.next().unwrap();
        let left_str = &left_str[1..];
        let left_str = left_str.trim();

        let right_str = tokens.next().unwrap();
        let right_str = &right_str[..right_str.len()-1];
        let right_str = right_str.trim();

        Self { 
            left_node: left_str.to_string(),  
            right_node: right_str.to_string(),
        }
    }
}

#[derive(PartialEq, Debug)]
pub struct Network {
    instructions: Vec<Instruction>,
    paths: HashMap<String, Choise>,
}

impl Network {
    pub fn get_steps_part1(&self) -> usize {
        return self.get_steps("AAA".to_string(), |s| -> bool {
            s.as_str() == "ZZZ"
        });
    }

    pub fn get_steps_part2(&self) -> usize {
        // Get Start nodes
        let start_nodes: Vec<String> = self.paths
            .keys()
            .filter(|node| node.ends_with("A"))
            .map(|node| node.clone())
            .collect();

        // For each start node, get steps
        let steps: Vec<usize> = start_nodes
            .into_iter()
            .map(|start_node| {
                self.get_steps(start_node, |s| s.ends_with("Z"))
            })
            .collect();

        // Get least common multiple
        let least_common_multiple = |v1: usize, v2: usize| -> usize {
            let min_val = std::cmp::min(v1, v2);
            let max_val = std::cmp::max(v1, v2);
            let mut value = min_val;

            while value < max_val {
                value += min_val;
            }

            loop {
                if value % min_val == 0 && value % max_val == 0 {
                    return value;
                }
                value += min_val;
            }
        };

        let mut iter = steps.iter();
        let mut step = iter.next().unwrap().clone();
        for &s in iter {
            step = least_common_multiple(step, s);
        }

        step
    }

    fn get_steps<Checker>(&self, start_node: String, is_target_node: Checker) -> usize 
    where Checker: Fn(&String) -> bool
    {
        let mut current_node = start_node;
        let mut steps: usize = 0;

        loop {
            for inst in self.instructions.iter() {
                if is_target_node(&current_node) {
                    return steps;
                }
                // Get choise
                let choise = self.paths.get(&current_node).unwrap();
                current_node = if *inst == Instruction::Left {
                    choise.left_node.clone()
                } else {
                    choise.right_node.clone()
                };
                steps += 1;
            }
        }
    }
}

impl From<&str> for Network {
    fn from(value: &str) -> Self {
        let mut lines = value.lines();

        // First line is instructions        
        let insts_line = lines.next().unwrap();
        let instructions: Vec<Instruction> = insts_line
            .chars()
            .map(|ch| {
                match ch {
                    'L' => Instruction::Left,
                    'R' => Instruction::Right,
                    _ => panic!("nonon"),
                }
            })
            .collect();

        // Empty line
        lines.next().unwrap();

        // Path lines
        let paths: HashMap<String, Choise> = lines
            .into_iter()
            .map(|line| {
                let mut tokens = line.split("=");
                let node_name = tokens.next().unwrap().trim().to_string();
                let choise = tokens.next().unwrap().trim().into();
                (node_name, choise)
            })
            .collect();

        Self { instructions, paths }
    }
}

#[cfg(test)]
mod test {
    use std::{collections::HashMap, vec};

    use crate::solution::Instruction;

    use super::{Choise, Network};

    #[test]
    fn test_str_to_choise() {
        let choise: Choise = " (AAA, ZZZ)".into();
        assert_eq!(
            choise,
            Choise { left_node: "AAA".to_string(), right_node: "ZZZ".to_string() }
        );
    }

    #[test]
    fn test_str_to_network() {
        let netwrok: Network = "LLR\n\nAAA = (BBB, BBB)\nBBB = (AAA, ZZZ)\nZZZ = (ZZZ, ZZZ)".into();
        assert_eq!(
            netwrok,
            Network {
                instructions: vec![Instruction::Left, Instruction::Left, Instruction::Right],
                paths: HashMap::from([
                    ("AAA".to_string(), Choise::new("BBB".to_string(), "BBB".to_string()) ),
                    ("BBB".to_string(), Choise::new("AAA".to_string(), "ZZZ".to_string()) ),
                    ("ZZZ".to_string(), Choise::new("ZZZ".to_string(), "ZZZ".to_string()) ),
                ])
            }
        )
    }
}